Skip to content

feat: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip] - #200

Merged
richm merged 2 commits into
mainfrom
fingerprint-write-to-file
Aug 6, 2026
Merged

feat: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip]#200
richm merged 2 commits into
mainfrom
fingerprint-write-to-file

Conversation

@spetrosi

@spetrosi spetrosi commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Feature: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip]

Reason: By default logs are printed to rsyslog. This change adds a possibility to write logs to a file on the system for the downstream users.

Result: For the upstream, this makes rsyslog log message more detailed. For the downstream - also writes logs to /var/log/sysroles.jsonl

@spetrosi
spetrosi requested a review from richm as a code owner August 6, 2026 12:47
@spetrosi spetrosi self-assigned this Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Ignore keyword(s) in the title.

⛔ Ignored keywords (1)
  • [citest_skip]

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b4c17755-ed4c-4609-86aa-615c37292a73

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Summary

The Ansible module now collects structured role fingerprints, formats them for syslog and JSONL, and supports locked file output with size trimming. Check mode returns fingerprint data without logging. Unit tests cover collection, formatting, storage, validation, and error handling.

Changes

Structured fingerprint logging

Layer / File(s) Summary
Fingerprint contract and formatting
library/sr_fingerprint.py, tests/unit/test_sr_fingerprint.py
The module replaces sr_message with structured arguments and defines fingerprint fields and separators. It collects role, status, Ansible, distribution, host-count, and check-mode data. Tests verify field coverage, formatting, derived values, quoting, and timestamps.
JSONL storage and trimming
library/sr_fingerprint.py, tests/unit/test_sr_fingerprint.py
Optional JSONL output creates parent directories, preserves value types and file metadata, uses exclusive sidecar locking, and removes oldest records when the size limit is exceeded. Tests cover appending, directory creation, trimming, multiline records, and disabled limits.
Handler validation and execution
library/sr_fingerprint.py, tests/unit/test_sr_fingerprint.py
The handler validates supported statuses and log sizes. Check mode returns fingerprint and prospective file data. Normal execution writes syslog and optional JSONL output, while write failures use fail_json. Tests cover these execution paths.

Suggested reviewers: richm, nhosoi

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description Format ⚠️ Warning The authored description has Reason:, Result:, and a valid Signed-off-by: line, but it lacks the required Enhancement: or Feature: section. Add an Enhancement: or Feature: section that describes the new fingerprint log-file functionality.
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title uses the required Conventional Commits format and accurately describes the fingerprint logging change.
Description check ✅ Passed The description explains the feature, reason, and result, but it omits the required issue-tracker section and uses Feature instead of Enhancement.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
tests/unit/test_sr_fingerprint.py (3)

195-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for a log file that already exceeds max_log_size.

Every trim test grows the file one record at a time, so the file never starts above the limit. That path hides the defect flagged on library/sr_fingerprint.py Lines 191-198, where _trim_log_file receives only the new-record size and leaves the existing excess in place.

Write several records with trimming disabled, then write one record with a small max_log_size, and assert that the resulting file size is at or below that limit.

💚 Proposed additional test
    def test_trim_shrinks_preexisting_oversized_file(self):
        with tempfile.NamedTemporaryFile(delete=False, suffix=".jsonl") as tmp:
            log_file = tmp.name

        try:
            record = _sample_fingerprint_record()
            line_size = len(sr_fingerprint._format_fingerprint_jsonl(record) + "\n")
            for _i in range(10):
                sr_fingerprint._write_jsonl_log(log_file, record, max_size=0)

            max_size = line_size * 3
            sr_fingerprint._write_jsonl_log(log_file, record, max_size=max_size)

            self.assertLessEqual(os.path.getsize(log_file), max_size)
        finally:
            _cleanup_log(log_file)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` around lines 195 - 219, Add a test
alongside test_trim_removes_oldest_lines that first writes several records with
trimming disabled (max_size=0), then writes one record using a small max_size,
and asserts os.path.getsize(log_file) is at or below that limit. Use the
existing temporary-file setup, sample record formatting, _write_jsonl_log, and
cleanup helpers.

172-177: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use shutil.rmtree for cleanup.

If _write_jsonl_log raises before it creates subdir, os.listdir(subdir) raises inside finally and hides the original failure. shutil.rmtree removes the tree unconditionally and keeps the real error visible.

♻️ Proposed refactor
         finally:
-            subdir = os.path.dirname(log_file)
-            for name in os.listdir(subdir):
-                os.unlink(os.path.join(subdir, name))
-            os.rmdir(subdir)
-            os.rmdir(tmpdir)
+            shutil.rmtree(tmpdir, ignore_errors=True)

Add import shutil to the imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` around lines 172 - 177, Update the cleanup
in the test’s finally block to use shutil.rmtree on the temporary directory,
adding the shutil import, so cleanup remains safe when subdir was never created
and does not mask the original _write_jsonl_log failure.

352-381: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for the normal syslog path.

No test runs _handle_fingerprint with check_mode=False and write_log_file=False. That is the default configuration and the primary documented behavior. _FakeModule.logged is populated at Line 39 but no test reads it, so a regression in the module.log call at library/sr_fingerprint.py Line 320 goes undetected.

💚 Proposed additional test
    def test_handle_fingerprint_logs_to_syslog_without_log_file(self):
        module = _FakeModule(
            {
                "status": "success",
                "write_log_file": False,
                "max_log_size": 2000000,
                "role_name": "systemd",
                "role_path": "/usr/share/ansible/roles/linux-system-roles.systemd",
                "ansible_play_hosts_all": ["host1", "host2"],
                "distribution": "RedHat",
                "distribution_version": "9.4",
            },
            check_mode=False,
        )
        with self.assertRaises(_ExitJsonException) as ctx:
            sr_fingerprint._handle_fingerprint(module)
        self.assertEqual(len(module.logged), 1)
        self.assertIn("status=success", module.logged[0])
        self.assertIn("play_hosts_number=2", module.logged[0])
        self.assertFalse(ctx.exception.kwargs["changed"])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` around lines 352 - 381, Add a unit test
alongside test_handle_fingerprint_write_failure_calls_fail_json that invokes
_handle_fingerprint with check_mode=False and write_log_file=False, then assert
module.logged contains one syslog message including status=success and
play_hosts_number=2, and that the resulting _ExitJsonException reports
changed=False.
library/sr_fingerprint.py (3)

300-327: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the validated max_log_size local.

Line 300 stores max_log_size, and Line 326 reads the same parameter again. Pass the local so the validated value and the used value cannot diverge later.

♻️ Proposed refactor
-            _write_jsonl_log(
-                log_file, fingerprint_record, module.params["max_log_size"]
-            )
+            _write_jsonl_log(log_file, fingerprint_record, max_log_size)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/sr_fingerprint.py` around lines 300 - 327, The validated max_log_size
local is not reused when writing the JSONL log. In the write_log_file branch,
update the _write_jsonl_log call to pass max_log_size instead of rereading
module.params["max_log_size"], while preserving the existing validation and
logging flow.

283-287: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

The syslog quoting convention is unpinned and untested. _format_fingerprint_key_value doubles an embedded " (CSV style) while common key=value log parsers expect backslash escaping, and it never escapes \. No test covers a value that contains ", =, or \, so the escaping branch is unverified. The PR adds these records for downstream consumers, so fix the convention before release.

  • library/sr_fingerprint.py#L283-L287: choose one documented convention and implement it; for logfmt, escape \ and " with a backslash and add \ to the trigger characters.
  • tests/unit/test_sr_fingerprint.py#L131-L140: add assertions for values that contain ", =, and \, matching the chosen convention.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/sr_fingerprint.py` around lines 283 - 287, Update
_format_fingerprint_key_value in library/sr_fingerprint.py:283-287 to use the
documented logfmt convention, triggering quoting for backslashes as well as
spaces, equals signs, and quotes, and escaping both backslashes and quotes with
a backslash. Add assertions in tests/unit/test_sr_fingerprint.py:131-140
covering values containing ", =, and \ and matching this convention.

193-208: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Read and write the log in binary mode.

Line 193 opens the file in text mode. Two problems follow:

  • Line 197 measures len(line) in characters, not bytes. The size limit is a byte limit. Any multi-byte content in the file makes the accounting wrong. Records written by this module are ASCII-escaped, but a pre-existing or externally written log is not guaranteed to be ASCII.
  • A non-decodable byte raises UnicodeDecodeError. That is a ValueError, not an OSError, so the handler at Line 328 does not catch it, and the module fails with a traceback instead of fail_json.

Use binary mode for both the read and the temporary write.

♻️ Proposed refactor
-    with open(log_file, "r") as log_fd:
+    with open(log_file, "rb") as log_fd:
         lines = log_fd.readlines()
@@
-        with os.fdopen(fd, "w") as tmp_fd:
+        with os.fdopen(fd, "wb") as tmp_fd:
             tmp_fd.writelines(lines)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/sr_fingerprint.py` around lines 193 - 208, Update the log rotation
logic around the open/read and temporary-file write operations to use binary
mode throughout. Preserve raw bytes when loading and writing lines so
size_removed uses byte lengths, arbitrary non-decodable content is supported,
and the existing fail_json error handling remains effective.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@library/sr_fingerprint.py`:
- Around line 191-198: Update the caller of _trim_log_file in
library/sr_fingerprint.py (lines 191-198) to pass the full size deficit,
cur_size plus len(new_line) minus max_size, so oversized files are trimmed until
they fit; add a regression test in tests/unit/test_sr_fingerprint.py (lines
195-219) that creates several records with max_size=0, appends one record using
a small max_log_size, and asserts the resulting file size does not exceed that
limit.

In `@tests/unit/test_sr_fingerprint.py`:
- Line 17: Update the test runner configuration used by
tests/unit/test_sr_fingerprint.py so PYTHONPATH includes library, allowing the
import sr_fingerprint to resolve in CI. Prefer adding PYTHONPATH=library to the
existing tox.ini test command without changing the test itself.

---

Nitpick comments:
In `@library/sr_fingerprint.py`:
- Around line 300-327: The validated max_log_size local is not reused when
writing the JSONL log. In the write_log_file branch, update the _write_jsonl_log
call to pass max_log_size instead of rereading module.params["max_log_size"],
while preserving the existing validation and logging flow.
- Around line 283-287: Update _format_fingerprint_key_value in
library/sr_fingerprint.py:283-287 to use the documented logfmt convention,
triggering quoting for backslashes as well as spaces, equals signs, and quotes,
and escaping both backslashes and quotes with a backslash. Add assertions in
tests/unit/test_sr_fingerprint.py:131-140 covering values containing ", =, and \
and matching this convention.
- Around line 193-208: Update the log rotation logic around the open/read and
temporary-file write operations to use binary mode throughout. Preserve raw
bytes when loading and writing lines so size_removed uses byte lengths,
arbitrary non-decodable content is supported, and the existing fail_json error
handling remains effective.

In `@tests/unit/test_sr_fingerprint.py`:
- Around line 195-219: Add a test alongside test_trim_removes_oldest_lines that
first writes several records with trimming disabled (max_size=0), then writes
one record using a small max_size, and asserts os.path.getsize(log_file) is at
or below that limit. Use the existing temporary-file setup, sample record
formatting, _write_jsonl_log, and cleanup helpers.
- Around line 172-177: Update the cleanup in the test’s finally block to use
shutil.rmtree on the temporary directory, adding the shutil import, so cleanup
remains safe when subdir was never created and does not mask the original
_write_jsonl_log failure.
- Around line 352-381: Add a unit test alongside
test_handle_fingerprint_write_failure_calls_fail_json that invokes
_handle_fingerprint with check_mode=False and write_log_file=False, then assert
module.logged contains one syslog message including status=success and
play_hosts_number=2, and that the resulting _ExitJsonException reports
changed=False.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cbe26997-0e8a-4ba0-8e4e-b1922d2c4c1b

📥 Commits

Reviewing files that changed from the base of the PR and between af4e494 and 8a69512.

📒 Files selected for processing (2)
  • library/sr_fingerprint.py
  • tests/unit/test_sr_fingerprint.py

Comment thread library/sr_fingerprint.py
Comment on lines +191 to +198
def _trim_log_file(log_file, size_needed):
"""Remove oldest records until the file can accommodate size_needed bytes."""
with open(log_file, "r") as log_fd:
lines = log_fd.readlines()
size_removed = 0
while lines and size_removed < size_needed:
size_removed += len(lines.pop(0))
orig_stat = os.stat(log_file)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

max_log_size is not enforced for an already-oversized file. The caller passes only the new-record size as size_needed, so _trim_log_file removes room for one record instead of removing the current excess. The unit tests only grow the file one record at a time, so they never reach the state that exposes the gap.

  • library/sr_fingerprint.py#L191-L198: pass the real deficit, cur_size + len(new_line) - max_size, so the loop trims until the file fits under max_size.
  • tests/unit/test_sr_fingerprint.py#L195-L219: add a test that writes several records with max_size=0, then writes one record with a small max_log_size, and asserts the resulting file size is at or below that limit.
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 192-192: File path is request-/variable-derived; validate and normalize to prevent path traversal.
Context: open(log_file, "r")
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(open-filename-from-request)

📍 Affects 2 files
  • library/sr_fingerprint.py#L191-L198 (this comment)
  • tests/unit/test_sr_fingerprint.py#L195-L219
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@library/sr_fingerprint.py` around lines 191 - 198, Update the caller of
_trim_log_file in library/sr_fingerprint.py (lines 191-198) to pass the full
size deficit, cur_size plus len(new_line) minus max_size, so oversized files are
trimmed until they fit; add a regression test in
tests/unit/test_sr_fingerprint.py (lines 195-219) that creates several records
with max_size=0, appends one record using a small max_log_size, and asserts the
resulting file size does not exceed that limit.

import tempfile
import unittest

import sr_fingerprint

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the test configuration and check for library/ on the path.
fd -H -t f 'tox.ini|pytest.ini|setup.cfg|pyproject.toml|conftest.py' \
  --exec rg -n 'PYTHONPATH|sys\.path|pythonpath|testpaths|library' {} \;

Repository: linux-system-roles/postgresql

Length of output: 167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked config/test helper files:"
git ls-files | rg '(^tox\.ini$|^pytest\.ini$|^setup\.cfg$|^pyproject\.toml$|^conftest\.py$|^tests/unit/)' | sed -n '1,120p'

echo
echo "All tracked files mentioning pytest/import/sr_fingerprint/pythonpath:"
rg -n 'pytest|importlib|sys\.path|PYTHONPATH|pythonpath|sr_fingerprint' -S --glob '!*.pyc' . | sed -n '1,200p'

echo
echo "tests/unit files (names only):"
git ls-files 'tests/unit/*' | sed -n '1,120p'

Repository: linux-system-roles/postgresql

Length of output: 4794


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "tox.ini:"
cat -n tox.ini

echo
echo "Python path-related searches in tox.ini:"
rg -n 'envlist|commands|setenv|pythonpath|PYTHONPATH|pytest|testpaths|addopts' tox.ini || true

echo
echo "Project-level files:"
git ls-files | rg '(^|/)(README\.md|README\.rst|tox\.ini|pytest\.ini|setup\.cfg|pyproject\.toml|Makefile|\.ci|\.github(/.*|))|(^\.ci/|(^|/)\.github/.*)' | sed -n '1,200p'

echo
echo "test files contents around imports:"
sed -n '1,80p' tests/unit/test_sr_fingerprint.py | cat -n

Repository: linux-system-roles/postgresql

Length of output: 3709


Set the unit-test runner path for sr_fingerprint.

tests/unit/test_sr_fingerprint.py imports library/sr_fingerprint.py directly, but tox.ini does not define the Python path. Add PYTHONPATH=library to the test command or a runner config so the test runs in CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/test_sr_fingerprint.py` at line 17, Update the test runner
configuration used by tests/unit/test_sr_fingerprint.py so PYTHONPATH includes
library, allowing the import sr_fingerprint to resolve in CI. Prefer adding
PYTHONPATH=library to the existing tox.ini test command without changing the
test itself.

@spetrosi spetrosi changed the title feat: Write roles fingerprints to /var/log/sysroles.jsonl feat: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip] Aug 6, 2026
Feature: Write roles fingerprints to /var/log/sysroles.jsonl [citest_skip]

Reason: By default logs are printed to rsyslog. This change adds a possibility to write logs to a file on the system for the downstream users.

Result: For the upstream, this makes rsyslog log message more detailed. For the downstream - also writes logs to /var/log/sysroles.jsonl
Signed-off-by: Sergei Petrosian <spetrosi@redhat.com>
@spetrosi
spetrosi force-pushed the fingerprint-write-to-file branch from 8a69512 to 28c15d2 Compare August 6, 2026 15:08
The sr_fingerprint module was rewritten to accept structured parameters
(status, role_name, role_path, etc.) instead of a free-form sr_message.
Update the role tasks and tests to match the new module interface.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@spetrosi

spetrosi commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

[citest]

@richm
richm merged commit 5b3a391 into main Aug 6, 2026
10 checks passed
@richm
richm deleted the fingerprint-write-to-file branch August 6, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants